/**
 * 
 */
package com.effectiv.gooruda.security.rest;

/**
 * @author Dhrubo
 *
 */

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Component;


import com.effectiv.gooruda.domain.Application;
import com.effectiv.gooruda.repository.ApplicationRepository;


import java.text.MessageFormat;
import java.util.ArrayList;

import jodd.cache.LFUCache;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class RestDaoAuthenticationProvider extends
		AbstractUserDetailsAuthenticationProvider {

	@Autowired
	private ApplicationRepository applicationRepository;
	
	private LFUCache<String,Application> applicationCache = new LFUCache<String,Application>(2000);

	@Autowired
	@Qualifier(value = "hmacPsswordEncoder")
	private PasswordEncoder passwordEncoder;

	/**
	 * This is the method which actually performs the check to see whether the
	 * user is indeed the correct user
	 * 
	 * @param userDetails
	 * @param authentication
	 * @throws AuthenticationException
	 */
	@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException { // here we check if the details
												// provided by the user actually
												// stack up.
		// Get Credentials out of the Token...
		RESTAuthenticationToken token = (RESTAuthenticationToken) authentication;
		if (token != null) {
			if (authentication.getCredentials() == null) {
				logger.debug("Authentication failed: no credentials provided");

				throw new BadCredentialsException(
						messages.getMessage(
								"AbstractUserDetailsAuthenticationProvider.badCredentials",
								"Bad credentials"));
			}
			
			
			
			RESTCredentials restCredentials = (RESTCredentials) authentication
					.getCredentials();

			log.debug("PASSWORD = {}", userDetails.getPassword());
			
			log.debug("userDetails = {}", userDetails);

			RESTUser restUser = (RESTUser)userDetails;
			
			if (!passwordEncoder.isPasswordValid(
					restCredentials.getSecureHash(), userDetails.getPassword(),
					restUser.getApplication().getSalt())) {
				logger.debug("Authentication failed: password does not match stored value");

				throw new BadCredentialsException(
						messages.getMessage(
								"AbstractUserDetailsAuthenticationProvider.badCredentials",
								"Bad credentials"));
			}

		} else {
			throw new AuthenticationCredentialsNotFoundException(
					MessageFormat
							.format("Expected Authentication Token object of type {0}, but instead received {1}",
									RESTAuthenticationToken.class
											.getSimpleName(), authentication
											.getClass().getSimpleName()));
		}
	}

	/**
	 * 
	 * @param apiKey
	 *            This is the API Key that was generated by the user. I guess
	 *            you could just use the users's username here, but we want
	 *            something that's a little less "guessable"
	 * 
	 * @param authentication
	 *            The authentication request, which subclasses <em>may</em> need
	 *            to perform a binding-based retrieval of the
	 *            <code>UserDetails</code>
	 * 
	 * @return the user information (never <code>null</code> - instead an
	 *         exception should the thrown)
	 * 
	 * @throws AuthenticationException
	 *             if the credentials could not be validated (generally a
	 *             <code>BadCredentialsException</code>, an
	 *             <code>AuthenticationServiceException</code> or
	 *             <code>UsernameNotFoundException</code>)
	 */
	@Override
	
	protected UserDetails retrieveUser(String publicKey,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		log.debug("Loading user by publicKey = {}", publicKey);
		
		//FIRST look in the cache
		Application application = applicationCache.get(publicKey) ;
		
		if(application == null){
			// Cache miss	
			log.debug("A cache miss.");
			application = applicationRepository.findByPublicKey(publicKey);
			
			
		}
		log.debug("Loaded application = {}", application);
		
		if (application == null) {
			throw new AuthenticationServiceException(
					"No application found for the given public key - " + publicKey);
		}
		
		applicationCache.put(publicKey, application);
		
		RESTUser restUser = new RESTUser(application.getName(), application.getPrivateKey(), application.getPublicKey(), new ArrayList<GrantedAuthority>());
		
		restUser.setApplication(application);
		
		return restUser;
	}

}
